﻿#region Copyright 2011 Mike Goatly
// This source is subject to the the MIT License (MIT)
// All rights reserved.
#endregion

namespace Lifti.Comparison
{
    #region Using Statements
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Xml.Linq;
    #endregion

    /// <summary>
    /// The entry point to the application.
    /// </summary>
    internal class Program
    {
        #region Constants
        /// <summary>
        /// The number of times to repeat the construction of the full text index before averaging the indexing time.
        /// </summary>
        private const int ConstructionCount = 20;

        /// <summary>
        /// The number of times to repeat the search for a word, before averaging the search time.
        /// </summary>
        private const int SearchCount = 1000;

        /// <summary>
        /// The words to search for.
        /// </summary>
        private static readonly string[] SearchWords = new[] { "Jack tells", "boars", "jack", "transmission", "wheelchair", "zebedee", "a" };
        #endregion

        /// <summary>
        /// The start-up procedure.
        /// </summary>
        /// <param name="args">The start-up arguments.</param>
        private static void Main(string[] args)
        {
            var episodes = LoadEpisodes();
            if (args.FirstOrDefault() == "/perftest")
            {
                TestLiftiWithStemmer(episodes, false);
            }
            else
            {
                OutputStats(episodes);

                // Test basic approach
                TestBasicApproach(episodes, true);

                // Test LIFTI
                TestBasicLiftiApproach(episodes, true);

                // Test LIFTI with stemmed word splitting
                TestLiftiWithStemmer(episodes, true);

                // Test the updatable full text index re-indexing/removing speeds
                TestUpdatableIndex(episodes);
            }
        }

        /// <summary>
        /// Tests the performance of an updatable index.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        private static void TestUpdatableIndex(IEnumerable<Episode> episodes)
        {
            IWordSplitter splitter = new StemmingWordSplitter();
            Console.WriteLine();
            Console.WriteLine("Updatable LIFTI update times:");
            TestUpdatableLiftiSpeed(episodes, splitter);
        }

        /// <summary>
        /// Tests performance of LIFTI with a stemming word splitter.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="testIndexSpeeds">If set to <c>true</c> then the indexing speed tests will be performed.</param>
        private static void TestLiftiWithStemmer(IEnumerable<Episode> episodes, bool testIndexSpeeds)
        {
            IWordSplitter splitter = new StemmingWordSplitter();
            Console.WriteLine();
            Console.WriteLine("LIFTI (stemmed words):");

            if (testIndexSpeeds)
            {
                TestLiftiIndexingSpeed(episodes, splitter);
            }

            TestLiftiSearchSpeed(episodes, splitter);
        }

        /// <summary>
        /// Tests the basic LIFTI approach.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="testIndexSpeeds">If set to <c>true</c> then the indexing speed tests will be performed.</param>
        private static void TestBasicLiftiApproach(IEnumerable<Episode> episodes, bool testIndexSpeeds)
        {
            IWordSplitter splitter = new WordSplitter();
            Console.WriteLine();
            Console.WriteLine("LIFTI:");
            if (testIndexSpeeds)
            {
                TestLiftiIndexingSpeed(episodes, splitter);
            }

            TestLiftiSearchSpeed(episodes, splitter);
        }

        /// <summary>
        /// Tests a basic approach, without using LIFTI.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="testIndexSpeeds">If set to <c>true</c> then the indexing speed tests will be performed.</param>
        private static void TestBasicApproach(IEnumerable<Episode> episodes, bool testIndexSpeeds)
        {
            Console.WriteLine();
            Console.WriteLine("Basic approach:");
            if (testIndexSpeeds)
            {
                TestBasicIndexingSpeed(episodes);
            }

            TestBasicSearchSpeed(episodes);
        }

        /// <summary>
        /// Tests the updatable LIFTI index/update/removal speed.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="splitter">The splitter.</param>
        private static void TestUpdatableLiftiSpeed(IEnumerable<Episode> episodes, IWordSplitter splitter)
        {
            var sw = new Stopwatch();
            sw.Start();
            var index = CreateUpdatableFullTextIndex(episodes, splitter);
            Console.WriteLine("Updatable full text index creation time: {0}ms", sw.ElapsedMilliseconds);

            sw.Reset();
            sw.Start();
            
            index.Index(episodes, e => e.ShortSummary);

            Console.WriteLine("Updatable full text index re-index time: {0}ms", sw.ElapsedMilliseconds);

            sw.Reset();
            sw.Start();

            index.Remove(episodes);

            Console.WriteLine("Updatable full text index complete removal time: {0}ms", sw.ElapsedMilliseconds);
            Console.WriteLine(index.Count);
        }

        /// <summary>
        /// Outputs the stats about the episodes.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        private static void OutputStats(IEnumerable<Episode> episodes)
        {
            var splitter = new WordSplitter();
            var episodeWords = (from e in episodes
                                select new { Episode = e, Words = splitter.SplitWords(e.ShortSummary).ToArray() }).ToArray();
            Console.WriteLine("Indexed item stats:");
            Console.WriteLine(
                "{0} items, {1:0.###} average words per item, {2} unique words",
                episodeWords.Length,
                episodeWords.Average(e => e.Words.Length),
                episodeWords.SelectMany(e => e.Words).Distinct().Count());
        }

        /// <summary>
        /// Tests LIFTI's searching speed.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="splitter">The word splitter to use.</param>
        private static void TestLiftiSearchSpeed(IEnumerable<Episode> episodes, IWordSplitter splitter)
        {
            var index = CreateFullTextIndex(episodes, splitter);

            RunIndexTests(index);
        }

        /// <summary>
        /// Runs the index tests.
        /// </summary>
        /// <param name="index">The index to run the tests against.</param>
        private static void RunIndexTests(IFullTextIndex<Episode> index)
        {
            var sw = new Stopwatch();
            foreach (string word in SearchWords)
            {
                sw.Reset();
                sw.Start();

                IEnumerable<Episode> results = null;
                for (int i = 0; i < SearchCount; i++)
                {
                    results = index.Search(word);
                }

                sw.Stop();
                Console.WriteLine("Search for: {0} - {1} iterations, {2} matches, {3:0.####}ms total time, {4:0.####}ms average time to results", word, SearchCount, results.Distinct().Count(), sw.ElapsedMilliseconds, (double)sw.ElapsedMilliseconds / SearchCount);
            }
        }

        /// <summary>
        /// Tests the Updatable LIFTI full text indexing speed when building up the index for the first time.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="splitter">The word splitter to use.</param>
        private static void TestUpdatableLiftiIndexingSpeed(IEnumerable<Episode> episodes, IWordSplitter splitter)
        {
            var sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < ConstructionCount; i++)
            {
                CreateUpdatableFullTextIndex(episodes, splitter);
            }

            sw.Stop();

            Console.WriteLine("Construction: {0} iterations, {1:0.####}ms total time, {2:0.####}ms average time to index", ConstructionCount, sw.ElapsedMilliseconds, (double)sw.ElapsedMilliseconds / ConstructionCount);
        }

        /// <summary>
        /// Tests the LIFTI full text indexing speed when building up the index for the first time.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="splitter">The word splitter to use.</param>
        private static void TestLiftiIndexingSpeed(IEnumerable<Episode> episodes, IWordSplitter splitter)
        {
            var sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < ConstructionCount; i++)
            {
                CreateFullTextIndex(episodes, splitter);
            }

            sw.Stop();

            Console.WriteLine("Construction: {0} iterations, {1:0.####}ms total time, {2:0.####}ms average time to index", ConstructionCount, sw.ElapsedMilliseconds, (double)sw.ElapsedMilliseconds / ConstructionCount);
        }

        /// <summary>
        /// Tests the search speed of the basic approach.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        private static void TestBasicSearchSpeed(IEnumerable<Episode> episodes)
        {
            var index = CreateWordLookup(episodes);

            var sw = new Stopwatch();
            foreach (string word in SearchWords)
            {
                sw.Reset();
                sw.Start();

                IEnumerable<Episode> results = null;
                for (int i = 0; i < SearchCount; i++)
                {
                    results = new Episode[0];
                    foreach (var item in index)
                    {
                        if (item.Key.StartsWith(word, StringComparison.Ordinal))
                        {
                            results = results.Union(item);
                        }
                    }
                }

                sw.Stop();
                Console.WriteLine("Search for: {0} - {1} iterations, {2} matches, {3:0.####}ms total time, {4:0.####}ms average time to results", word, SearchCount, results.Count(), sw.ElapsedMilliseconds, (double)sw.ElapsedMilliseconds / SearchCount);
            }
        }

        /// <summary>
        /// Tests the indexing speed of the basic approach (not very interesting).
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        private static void TestBasicIndexingSpeed(IEnumerable<Episode> episodes)
        {
            var sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < ConstructionCount; i++)
            {
                CreateWordLookup(episodes);
            }

            sw.Stop();

            Console.WriteLine("Construction: {0} iterations, {1:0.####}ms total time,  {2:0.####}ms average time to index", ConstructionCount, sw.ElapsedMilliseconds, (double)sw.ElapsedMilliseconds / ConstructionCount);
        }

        /// <summary>
        /// Creates a full text index for the given episodes.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="splitter">The word splitter to use.</param>
        /// <returns>The full text index.</returns>
        private static FullTextIndex<Episode> CreateFullTextIndex(IEnumerable<Episode> episodes, IWordSplitter splitter)
        {
            var indexer = new FullTextIndex<Episode>() { WordSplitter = splitter };
            indexer.Index(episodes, e => e.ShortSummary);
            return indexer;
        }

        /// <summary>
        /// Creates a full text index for the given episodes.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <param name="splitter">The word splitter to use.</param>
        /// <returns>The full text index.</returns>
        private static UpdatableFullTextIndex<Episode> CreateUpdatableFullTextIndex(IEnumerable<Episode> episodes, IWordSplitter splitter)
        {
            var indexer = new UpdatableFullTextIndex<Episode>() { WordSplitter = splitter };
            indexer.Index(episodes, e => e.ShortSummary);
            return indexer;
        }

        /// <summary>
        /// Creates a lookup for the given episodes, keyed against words found in their summary.
        /// </summary>
        /// <param name="episodes">The episodes.</param>
        /// <returns>The constructed lookup.</returns>
        private static ILookup<string, Episode> CreateWordLookup(IEnumerable<Episode> episodes)
        {
            var splitter = new WordSplitter();
            return (from e in episodes
                    from w in splitter.SplitWords(e.ShortSummary)
                    select new { Word = w.Word, Episode = e }).ToLookup(w => w.Word.ToLower(), w => w.Episode);
        }

        /// <summary>
        /// Loads the episodes from the embedded XML file.
        /// </summary>
        /// <returns>The loaded episodes</returns>
        private static IEnumerable<Episode> LoadEpisodes()
        {
            using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(Program), "Episodes.xml"))
            {
                using (var reader = new StreamReader(stream))
                {
                    var xmlDoc = XDocument.Load(reader);

                    return (from e in xmlDoc.Descendants("Episode")
                            select new Episode()
                            {
                                DirectedBy = e.Element("DirectedBy").Value,
                                EpisodeNumber = e.Element("EpisodeNumber").Value,
                                OriginalAirDate = e.Element("OriginalAirDate").Value,
                                Season = Int32.Parse(e.Element("Season").Value),
                                ShortSummary = e.Element("ShortSummary").Value,
                                Title = e.Element("Title").Value,
                            }).ToArray();
                }
            }
        }
    }
}
